import os
import sys
import time
import socket
import pickle
import random
import threading

class Peer(threading.Thread):
    ##Initiate the peer.
    def __init__(self, name, neighbors, port):
        threading.Thread.__init__(self)        
        self._name = name
        self._neighbors = neighbors
        self._port = port
        self._plist = [(name, neighbors, "127.0.0.1", port)]
        self._nlist = []
        self._dir = os.getcwd() + "/files/"
        self._flist = [name]

    ##Hello method which also searches for neighbors
    def hello(self, ip, port):
        self.traverseplist(self.boot(ip, port))
        self.getneighbors()

    ##Bootstrapping for Hello method for peer discovery with bootstrap ip
    def boot(self, ip, port):
        out = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        out.connect((ip, port))
        out.send("hello")
        out.send(pickle.dumps(self._plist[0]))
        out.shutdown(socket.SHUT_WR)

        ##Receive plist from connected peer
        data = ""
        while 1:
            inp = out.recv(1024)
            if not inp: break
            data += inp
        out.close()
        ##Reconstruct bufferedplist from bootstrapped ip and return
        return self.construct(data)
        
    ##Traverse each peer in plist with "Hello" requests - recursive for the win
    def traverseplist(self, plist):
        print self._plist
        for name, neighbors, ip, port in plist:
            print ip
            print port
            if not self.isintuplelist(name, self._plist):
                self._plist.append((name, neighbors, ip, port))
                self.traverseplist(self.boot(ip, port))

    ##Get neighbors by asking peers with most to fewest max neighbors.
    def getneighbors(self):
        i = 0
        priolist = self.createprioritylist()
        while i < len(priolist) and len(self._nlist) != self._neighbors:
            name, neighbors, ip, port = priolist[i]
            out = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            out.connect((ip, port))
            out.send("neighbor")
            out.send(pickle.dumps(self._plist[0]))
            out.shutdown(socket.SHUT_WR)
            ##Receive reply from peer
            data = ""
            while 1:
                inp = out.recv(1024)
                if not inp: break
                data += inp
            out.close()
            ##Interpret data and add neighbor
            if data.find("accepted") != -1:
                self._nlist.append(priolist[i])
            i += 1
        
    ##Creates a list with negihborhood priorities
    def createprioritylist(self):
        priolist = list(self._plist)
        priolist.pop(0)
        priolist.sort(self.compare)
        return priolist
    
    ##Compare two peers to each other based on the max number of neighbors - used by createprioritylist
    def compare(self, p1, p2):
        return cmp(p1[1], p2[1])
        
    ## Pings a neighbor every 15 seconds to check if said neighbor is still alive. If they aren't they are removed from the nlist
    def pingneighbors(self):
        i = 0
        while 1:
            if len(self._nlist) != 0:
                name, neighbors, ip, port = self._nlist[i % len(self._nlist)]
                out = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
                out.connect((ip, port))
                out.send("ping")
                out.send(pickle.dumps(self._flist))
                out.shutdown(socket.SHUT_WR)
                data = ""
                while 1:
                    out.settimeout(5.0)
                    inp = out.recv(1024)
                    if not inp: break
                    data += inp
                if data is "":
                    print "neighbor removed: " + str(self._nlist[i % len(self._nlist)])
                    self._nlist.pop(i % len(self._nlist))
                out.close()
                i += 1
                print data
            time.sleep(60)
            
    ##Deconstruct plist
    def deconstruct(self, alist):
        plistpickle = pickle.dumps(alist)
        return plistpickle

    ##Reconstruct plist
    def construct(self, plistpickle):
        plist = pickle.loads(plistpickle)
        return plist

    ##Method for checking whether peer is already present in a given 4-tuple list
    def isintuplelist(self, name, list):
        for name2, neighbors, ip, port in list:
            if name == name2:
                return 1
        return 0

    ##Method for creating graphviz document
    def creategraph(self, peers):
        selectedpeers = []
        neighborhood = []
        for name, neighbors, ip, port in self._plist:
            for p in peers:
                if name == p:
                    selectedpeers.append((name, neighbors, ip, port))
        for apeer in selectedpeers:
             nlist = self.getnlist(apeer)
             for npeer in nlist:
                 if not (apeer, npeer) in neighborhood and not (npeer, apeer) in neighborhood and npeer in selectedpeers:
                     neighborhood.append((apeer, npeer))
        graph = "graph network {\n"
        for name, neighbors, ip, port in selectedpeers:
            graph += "\"" + name + "(" + str(neighbors) + ")\";\n"
        for apeer, npeer in neighborhood:
            graph += "\"" + apeer[0] + "(" + str(apeer[1]) + ")\" -- \"" + npeer[0] + "(" + str(npeer[1]) + ")\";\n"
        graph += "}\n"
        return graph
        
    ##Method for tedious socket opening and closing to retrieve nlist for creategraph method
    def getnlist(self, peer):
        name, neighbors, ip, port = peer
        out = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        out.connect((ip, port))
        out.send("nlist")
        out.shutdown(socket.SHUT_WR)
        data = ""
        while 1:
            inp = out.recv(1024)
            if not inp: break
            data += inp
        out.close()
        nlist = pickle.loads(data)
        return nlist

    ##Create flist with random number of files from a given directory. Files are represented by 2-tuples
    def addrandomfiles(self):
        files = os.listdir(self._dir)
        amount = random.randint(1, len(files))
        for i in range(amount):
            name, size = files[i], os.stat(self._dir + files[i]).st_size
            self._flist.append((name, size))

    ##Listening method running in thread to constantly listen for requests
    def listen(self):
        inp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        inp.bind(("", self._port))
        inp.listen(5)
        while 1:
            data=""
            conn, addr = inp.accept()
            while 1:
                randominput = conn.recv(1024)
                if not randominput: break
                data += randominput

            ##If request is a "Hello" message, return plist
            if data.find("hello") != -1:
                conn.send(self.deconstruct(self._plist))
                conn.shutdown(socket.SHUT_WR)
                ##Retrieve senders info, add to plist if not already present
                buff = pickle.loads(data[5:])
                if not self.isintuplelist(buff[0], self._plist):
                    self._plist.append(buff)

            ##Check whether peer is able to accept a new neighbor, add if possible
            if data.find("neighbor") != -1:
                buff = pickle.loads(data[8:])
                if len(self._nlist) < self._neighbors:
                    if not self.isintuplelist(buff[0], self._nlist):
                        conn.send("accepted")
                        self._nlist.append(buff)
                conn.shutdown(socket.SHUT_WR)

            ##Return neighborhood list
            if data == "nlist":
                conn.send(self.deconstruct(self._nlist))
                conn.shutdown(socket.SHUT_WR)

            ##Reply pong to acknowledge peer is alive
            if data.find("ping") != -1:
                ##TODO: We get the peers flist here. Implement handling of this.
                conn.send("pong")
                conn.shutdown(socket.SHUT_WR)
                print data
            conn.close()

##Main method including the basic user interface to interact with program
def main(name, neighbors, port):
    peer = Peer(name, int(neighbors), int(port))
    listen = threading.Thread(name="listen", target=peer.listen)
    listen.setDaemon(1)
    #ping = threading.Thread(name="ping", target=peer.pingneighbors)
    #ping.setDaemon(1)
    
    listen.start()
    #ping.start()

    while 1:
        try:
          strings = raw_input(name + "> ").split()
        except EOFError:
          break
          
        if strings[0] == "hello":
            ip, port = strings[1].split(":")
            print ip
            print port
            peer.hello(ip, int(port))
            
        elif strings[0] == "plist":
            print peer._plist
        
        elif strings[0] == "nlist" and len(strings) > 1:
            peers = []
            i = 1
            while i < len(strings):
                if strings[i] != "-o":
                    peers.append(strings[i])
                elif strings[i] == "-o":
                    f = open(strings[i + 1], "w")
                    f.write(peer.creategraph(peers))
                    f.close()
                i += 1

        elif strings[0] == "nlist":
            print peer._nlist
        
        elif strings[0] == "flist":
            peer.addrandomfiles()
            print peer._flist

        elif strings[0] == "find":
            print "Not yet implemented"
        
        elif strings[0] == "exit":
            sys.exit(0)
         
        elif strings[0] == "continue":
            while 1: pass

if __name__ == "__main__":
    main(sys.argv[1], sys.argv[2], sys.argv[3])
